import { constants,
  kAfterAsyncWrite,
  kHandle,
  kID,
  kInfoHeaders,
  kInit,
  kProceed,
  kProxySocket,
  kRequest,
  kSentHeaders,
  kSentTrailers,
  kSession, kState, kTimeout,
  kUpdateTimer,
  kWriteGeneric,
  STREAM_FLAGS_ABORTED,
  STREAM_FLAGS_CLOSED,
  STREAM_FLAGS_HEADERS_SENT,
  STREAM_FLAGS_HEAD_REQUEST,
  STREAM_FLAGS_PENDING,
  STREAM_FLAGS_READY} from '../constant/Constant';
import { getStreamState, validateFunction, validateInt32 } from '../utils';

export class Http2Stream {
  constructor(session, options) {
    options.allowHalfOpen = true;
    options.decodeStrings = false;
    options.autoDestroy = false;
    // super();
    // this[async_id_symbol] = -1;
    //
    // // Corking the stream automatically allows writes to happen
    // // but ensures that those are buffered until the handle has
    // // been assigned.
    // this.cork();
    this[kSession] = session;
    session[kState].pendingStreams.add(this);

    // Allow our logic for determining whether any reads have happened to
    // work in all situations. This is similar to what we do in _http_incoming.
    // this._readableState.readingMore = true;

    this[kTimeout] = null;

    this[kState] = {
      didRead: false,
      flags: STREAM_FLAGS_PENDING,
      rstCode: constants.NGHTTP2_NO_ERROR,
      writeQueueSize: 0,
      trailersReady: false,
      endAfterHeaders: false,
    };

    // Fields used by the compat API to avoid megamorphisms.
    this[kRequest] = null;
    this[kProxySocket] = null;

    // this.on('pause', streamOnPause);
    //
    // this.on('newListener', streamListenerAdded);
    // this.on('removeListener', streamListenerRemoved);
  }

  writableLength: number;
  rstCode: number = 0;
  push(buf) {
  };

  [kUpdateTimer]() {
    // if (this.destroyed)
    //   return;
    if (this[kTimeout]) {
      this[kTimeout].refresh();
    }
    if (this[kSession]) {
      this[kSession][kUpdateTimer]();
    }
  }

  [kInit](id, handle) {
    const state = this[kState];
    state.flags |= STREAM_FLAGS_READY;

    const session = this[kSession];
    session[kState].pendingStreams.delete(this);
    session[kState].streams.set(id, this);

    this[kID] = id;
  }


  get bufferSize() {
    // `bufferSize` properties of `net.Socket` are `undefined` when
    // their `_handle` are falsy. Here we avoid the behavior.
    return this[kState].writeQueueSize + this.writableLength;
  }

  get endAfterHeaders() {
    return this[kState].endAfterHeaders;
  }

  get sentHeaders() {
    return this[kSentHeaders];
  }

  get sentTrailers() {
    return this[kSentTrailers];
  }

  get sentInfoHeaders() {
    return this[kInfoHeaders];
  }

  get pending() {
    return this[kID] === undefined;
  }

  // The id of the Http2Stream, will be undefined if the socket is not
  // yet connected.
  get id() {
    return this[kID];
  }

  // The Http2Session that owns this Http2Stream.
  get session() {
    return this[kSession];
  }

  _onTimeout() {
    // callTimeout(this, this[kSession]);
  }

  // True if the HEADERS frame has been sent
  get headersSent() {
    return !!(this[kState].flags & STREAM_FLAGS_HEADERS_SENT);
  }

  // True if the Http2Stream was aborted abnormally.
  get aborted() {
    return !!(this[kState].flags & STREAM_FLAGS_ABORTED);
  }

  // True if dealing with a HEAD request
  get headRequest() {
    return !!(this[kState].flags & STREAM_FLAGS_HEAD_REQUEST);
  }

  // State information for the Http2Stream
  get state() {
    const id = this[kID];
    // if (this.destroyed || id === undefined)
    //   return {};
    return getStreamState(this[kHandle]);
  }


  [kAfterAsyncWrite]({ bytes }) {
    this[kState].writeQueueSize -= bytes;

    if (this.session !== undefined) {
      this.session[kState].writeQueueSize -= bytes;
    }
  }

  [kWriteGeneric](writev, data, encoding, cb) {
    // When the Http2Stream is first created, it is corked until the
    // handle and the stream ID is assigned. However, if the user calls
    // uncork() before that happens, the Duplex will attempt to pass
    // writes through. Those need to be queued up here.
    if (this.pending) {
      // this.once(
      //   'ready',
      //   this[kWriteGeneric].bind(this, writev, data, encoding, cb),
      // );
      return;
    }

    // If the stream has been destroyed, there's nothing else we can do
    // because the handle has been destroyed. This should only be an
    // issue if a write occurs before the 'ready' event in the case where
    // the duplex is uncorked before the stream is ready to go. In that
    // case, drop the data on the floor. An error should have already been
    // emitted.
    // if (this.destroyed)
    //   return;

    this[kUpdateTimer]();
    if (!this.headersSent) {
      this[kProceed]();
    }

    let req;

    let waitingForWriteCallback = true;
    let waitingForEndCheck = true;
    let writeCallbackErr;
    let endCheckCallbackErr;
    const done = () => {
      if (waitingForEndCheck || waitingForWriteCallback) {
        return;
      }
    };
    const writeCallback = (err) => {
      waitingForWriteCallback = false;
      writeCallbackErr = err;
      done();
    };
    const endCheckCallback = (err) => {
      waitingForEndCheck = false;
      endCheckCallbackErr = err;
      done();
    };

  }

  _write(data, encoding, cb) {
    this[kWriteGeneric](false, data, encoding, cb);
  }

  _writev(data, cb) {
    this[kWriteGeneric](true, data, '', cb);
  }

  _final(cb) {

  }

  _read(nread) {

  }

  priority(options) {

  }

  sendTrailers(headers) {

  }

  get closed() {
    return !!(this[kState].flags & STREAM_FLAGS_CLOSED);
  }

  // Close initiates closing the Http2Stream instance by sending an RST_STREAM
  // frame to the connected peer. The readable and writable sides of the
  // Http2Stream duplex are closed and the timeout timer is cleared. If
  // a callback is passed, it is registered to listen for the 'close' event.
  //
  // If the handle and stream ID have not been assigned yet, the close
  // will be queued up to wait for the ready event. As soon as the stream ID
  // is determined, the close will proceed.
  //
  // Submitting the RST_STREAM frame to the underlying handle will cause
  // the Http2Stream to be closed and ultimately destroyed. After calling
  // close, it is still possible to queue up PRIORITY and RST_STREAM frames,
  // but no DATA and HEADERS frames may be sent.
  close(code = constants.NGHTTP2_NO_ERROR, callback) {
    validateInt32(code, 'code', 0);

    if (callback !== undefined) {
      validateFunction(callback, 'callback');
    }

    if (this.closed) {
      return;
    }

  }

  // Called by this.destroy().
  // * Will submit an RST stream to shutdown the stream if necessary.
  //   This will cause the internal resources to be released.
  // * Then cleans up the resources on the js side
  _destroy(err, callback) {
    const session = this[kSession];
    const handle = this[kHandle];
    const id = this[kID];

    // debugStream(this[kID] || 'pending', session[kType], 'destroying stream');

    const state = this[kState];
    const sessionState = session[kState];
    const sessionCode = sessionState.goawayCode || sessionState.destroyCode;


    const hasHandle = handle !== undefined;

    // if (!this.closed)
    //   closeStream(this, code, hasHandle ? kForceRstStream : kNoRstStream);
    this.push(null);

    if (hasHandle) {
      handle.destroy();
      sessionState.streams.delete(id);
    } else {
      sessionState.pendingStreams.delete(this);
    }

    // Adjust the write queue size for accounting
    sessionState.writeQueueSize -= state.writeQueueSize;
    state.writeQueueSize = 0;

    // RST code 8 not emitted as an error as its used by clients to signify
    // abort and is already covered by aborted event, also allows more
    // seamless compatibility with http1
    // if (err == null && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL)
    //   err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code);

    this[kSession] = undefined;
    this[kHandle] = undefined;

    // This notifies the session that this stream has been destroyed and
    // gives the session the opportunity to clean itself up. The session
    // will destroy if it has been closed and there are no other open or
    // pending streams. Delay with setImmediate so we don't do it on the
    // nghttp2 stack.
    // setImmediate(() => {
    //   session[kMaybeDestroy]();
    // });
    callback(err);
  }

}